﻿//     Copyright (c) Microsoft Corporation.  All rights reserved.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Configuration;
using System.Runtime.Serialization.Formatters;
using Microsoft.Gateway.Configuration;
using Microsoft.Gateway.Utils;
using Microsoft.Web.Administration;

using ConfigurationElement = Microsoft.Web.Administration.ConfigurationElement;
using ConfigurationElementCollection = Microsoft.Web.Administration.ConfigurationElementCollection;

namespace Microsoft.Gateway.RulesConfigurator
{
    internal class WebsiteConfigurator
    {
        //Config settings
        const string ConfigKeyRulesPrefix = "RulesPrefix";
        const string ConfigKeyServerVariableOriginalHttpHost = "ServerVariableOriginalHttpHostName";

        // Server variables
        const string SERVER_VARIABLE_TARGET_URI                         = "TARGET_URI";
        const string SERVER_VARIABLE_CLIENT_REQUEST_ID                  = "HTTP_client_request_Id";
        const string SERVER_VARIABLE_REMOTE_ADDR                        = "REMOTE_ADDR";
        const string SERVER_VARIABLE_HTTP_COOKIE                        = "HTTP_COOKIE";
        const string SERVER_VARIABLE_CACHE_URL                          = "CACHE_URL";
        const string SERVER_VARIABLE_ACCEPT_ENCODING                    = "HTTP_ACCEPT_ENCODING";
        const string SERVER_VARIABLE_ORIGINAL_ACCEPT_ENCODING           = "ORIGINAL_HTTP_ACCEPT_ENCODING";
        const string SERVER_VARIABLE_RESPONSE_CONTENT_TYPE              = "RESPONSE_CONTENT_TYPE";
        const string SERVER_VARIABLE_RESPONSE_STATUS                    = "RESPONSE_STATUS";
        const string SERVER_VARIABLE_RESPONSE_LOCATION                  = "RESPONSE_LOCATION";
        const string SERVER_VARIABLE_OUTBOUND_RULE_FILTER               = "OUTBOUND_RULE_FILTER";
        const string SERVER_VARIABLE_HTTP_X_REMOTE_USER                 = "HTTP_X_REMOTE_USER";
        const string SERVER_VARIABLE_HTTP_X_TENANT                      = "HTTP_X_TENANT";
        const string SERVER_VARIABLE_PROCESS_OUTBOUND_CONTENT_REWRITE   = "PROCESS_OUTBOUND_CONTENT_REWRITE";
        const string SERVER_VARIABLE_PROCESS_OUTBOUND_CONTENT_REWRITE_RETURNURL = "PROCESS_OUTBOUND_CONTENT_REWRITE_RETURNURL";
        const string SERVER_VARIABLE_PROCESS_OUTBOUND_CONTENT_BADCHARS  = "PROCESS_OUTBOUND_CONTENT_BADCHARS";
        const string SERVER_VARIABLE_PROCESS_OUTBOUND_LOCATION_REDIRECT = "PROCESS_OUTBOUND_LOCATION_REDIRECT";
        const string SERVER_VARIABLE_PROCESS_RESTORE_ACCEPT_ENCODING    = "PROCESS_RESTORE_ACCEPT_ENCODING";
        const string SERVER_VARIABLE_INTERNAL_URLS                      = "INTERNAL_URLS";
        const string SERVER_VARIABLE_GATEWAY_ROOT_PATH                  = "GATEWAY_ROOT_PATH";
        const string SERVER_VARIABLE_GATEWAYSERVER_ROOT_PATH            = "WEBSERVER_ROOT_PATH";
        const string SERVER_VARIABLE_HTTP_HOST                          = "HTTP_HOST";
        const string SERVER_VARIABLE_ORIGINAL_HTTP_HOST                 = "ORIGINAL_HTTP_HOST";

        const string ELEMENT_OUTBOUND_REMOVE_BADCHARS_PRECONDITION      = "Remove <![ Characters Temporarily";
        const string ELEMENT_OUTBOUND_REMOVE_BADCHARS_RULE_PROBLEM_STRING_BEING = @"<!\["; // add the \ to escape the regex special character [
        const string ELEMENT_OUTBOUND_REMOVE_BADCHARS_RULE_PROBLEM_STRING_END = @"<![";
        const string ELEMENT_OUTBOUND_REMOVE_BADCHARS_RULE_TEMP_STRING  = "!Replace_Breaking_String!";

        const string DEFAULT_CONFIG_KEY_RULES_PREFIX                    = "Gateway_";

        // regex generation
        const string SLASH_DOT                                          = @"\.";
        const string REGEX_0_TO_255                                     = "([0]|[1-9]\\d?|[1]\\d{2}|2([0-4]\\d|5[0-5]))";
        const string RETURNURL_PARAMETER_NAMES                          = "ReturnUrl|wreply|redirect_uri|openid.return_to";

        static readonly string[] AllowedServerVariables = new[]
        {
            SERVER_VARIABLE_TARGET_URI,
            SERVER_VARIABLE_CLIENT_REQUEST_ID,
            SERVER_VARIABLE_ACCEPT_ENCODING,
            SERVER_VARIABLE_ORIGINAL_ACCEPT_ENCODING,
            SERVER_VARIABLE_OUTBOUND_RULE_FILTER,
            SERVER_VARIABLE_HTTP_X_REMOTE_USER,
            SERVER_VARIABLE_HTTP_X_TENANT,
            SERVER_VARIABLE_PROCESS_OUTBOUND_CONTENT_REWRITE,
            SERVER_VARIABLE_PROCESS_OUTBOUND_CONTENT_REWRITE_RETURNURL,
            SERVER_VARIABLE_PROCESS_OUTBOUND_CONTENT_BADCHARS,
            SERVER_VARIABLE_PROCESS_OUTBOUND_LOCATION_REDIRECT,
            SERVER_VARIABLE_PROCESS_RESTORE_ACCEPT_ENCODING,
            SERVER_VARIABLE_INTERNAL_URLS,
            SERVER_VARIABLE_GATEWAY_ROOT_PATH,
            SERVER_VARIABLE_GATEWAYSERVER_ROOT_PATH,
            SERVER_VARIABLE_ORIGINAL_HTTP_HOST,
        };

        public static ConfiguratorResults ApplyConfiguration(RulesConfiguration configToApply, string configPath = null)
        {
            try
            {
                if (!String.IsNullOrWhiteSpace(configPath))
                {
                    configPath = Path.Combine(Path.GetDirectoryName(Path.GetFullPath(configPath)), "applicationHost.config");
                }
                using (ServerManager serverManager = new ServerManager(configPath))
                {
                    var globalConfig = serverManager.GetApplicationHostConfiguration();
                    var webFarmsCollection = WebfarmConfigurator.ConfigureWebFarmsNodeDefaults(globalConfig);

                    // Make sure our <allowedServerVariables> is complete
                    var allowedVariablesCollection =
                        globalConfig.GetSection("system.webServer/rewrite/allowedServerVariables").GetCollection();
                    var existingVariables = allowedVariablesCollection
                        .Select(variableElement => variableElement["name"].ToString())
                        .ToLookup(variable => variable, StringComparer.OrdinalIgnoreCase);
                    foreach (string allowedVariable in AllowedServerVariables
                                                            .Where(variable => !existingVariables.Contains(variable)))
                    {
                        var variableElement = allowedVariablesCollection.CreateElement("add");
                        variableElement["name"] = allowedVariable;
                        allowedVariablesCollection.Add(variableElement);
                    }

#if _USE_GLOBAL_RULES
                    var webConfig = globalConfig;
                    string rulesSection = "globalRules";
#else // _USE_GLOBAL_RULES
                    string siteName = AzureUtils.GetDefaultIISSiteName();
                    if (serverManager.Sites.FirstOrDefault(site => siteName.Equals(site.Name, StringComparison.OrdinalIgnoreCase)) == null)
                    {
                        Trace.TraceInformation("WebsiteConfigurator: website not available yet, so not running configuration.");
                        return ConfiguratorResults.RetryLater;
                    }
                    var webConfig = serverManager.GetWebConfiguration(siteName);
                    string rulesSection = "rules";
#endif
                    var inboundRulesCollection =
                        webConfig.GetSection("system.webServer/rewrite/" + rulesSection).GetCollection();
                    var outboundRulesSection = webConfig.GetSection("system.webServer/rewrite/outboundRules");
                    var outboundRulesCollection = outboundRulesSection.GetCollection();
                    var preConditionsCollection = outboundRulesSection.GetCollection("preConditions");

                    ClearExistingRules(preConditionsCollection, inboundRulesCollection, outboundRulesCollection);

                    //Add all rules that must be processed first
                    inboundRulesCollection.Add(
                        CreateInboundGwRemoteUserRule(inboundRulesCollection.CreateElement("rule")));

                    // Apply role rules in alphabetical order
                    foreach (var role in configToApply.OrderedRoles)
                    {
                        if (role.Item2.Flights.Any())
                        {
                            foreach (var flight in role.Item2.Flights)
                            {
                                //ARR Rule
                                if (flight.IsArrFlight)
                                {
                                    WebfarmConfigurator.CreateWebFarm(webFarmsCollection, role.Item2, flight);

                                    //Create one inbound rule for each webfarm created in ARR 
                                    inboundRulesCollection.Add(
                                        CreateFlightRuleARR(inboundRulesCollection.CreateElement("rule"), role.Item2,
                                            flight));

                                }
                                else //Non-ARR Rule
                                {
                                    inboundRulesCollection.Add(CreateFlightRule(
                                        inboundRulesCollection.CreateElement("rule"), role.Item1, role.Item2,
                                        flight.Name, flight));
                                }
                            }
                        }
                        else // Non-flight rule
                        {
                            if (role.Item2.IsArrBaseTarget)
                            {
                                WebfarmConfigurator.CreateWebFarm(webFarmsCollection, role.Item2);
                            }

                            inboundRulesCollection.Add(CreateRoleRule(
                                inboundRulesCollection.CreateElement("rule"), role.Item1, role.Item2));
                        }
                    }

                    preConditionsCollection.Add(CreatePrecondition(
                        preConditionsCollection.CreateElement("preCondition"),
                        string.Format("{0}{1}", RulesPrefix(), ELEMENT_OUTBOUND_REMOVE_BADCHARS_PRECONDITION),
                        Tuple.Create(SERVER_VARIABLE_RESPONSE_CONTENT_TYPE, "^text/html"),
                        Tuple.Create(SERVER_VARIABLE_PROCESS_OUTBOUND_CONTENT_BADCHARS, "^1$")));

                    // Create outbound rules
                    CreateOutboundRemoveBadcharsRuleBegin(outboundRulesCollection);
                    CreateOutboundRewriteRule(outboundRulesCollection, preConditionsCollection);
                    CreateOutboundReturnUrlRewriteRule(outboundRulesCollection, preConditionsCollection);
                    CreateOutboundRemoveBadcharsRuleEnd(outboundRulesCollection);
                    CreateOutboundLocationRedirectRule(outboundRulesCollection, preConditionsCollection);
                    CreateOutboundRestoreAcceptEncodingRule(outboundRulesCollection, preConditionsCollection);

                    serverManager.CommitChanges();
                    Trace.TraceInformation("WebsiteConfigurator: Updated routing configuration successfully applied for new Role configuration.");
                }
                return ConfiguratorResults.Success;
            }
            catch (Exception ex)
            {
                Trace.TraceError("Failed to update routing rules for web server. Details: {0}", ex);
            }
            return ConfiguratorResults.Failure;
        }

        private static ConfigurationElement CreateFlightRule(ConfigurationElement ruleElement, string roleName,
            Role role, string flightName, Flight flight)
        {
            string internalUrls = flight.GetEffectiveRedirectBase(role).GetDomainName();
            internalUrls += ";{HTTP_HOST}";

            CreateInboundRule(ruleElement, roleName, role, flightName, flight.GetEffectiveRedirectBase(role),
                role.TargetBasePath, internalUrls,
                GetFlightConditions(flight));
            return ruleElement;
        }

        private static Action<ConfigurationElementCollection> GetFlightConditions(Flight flight)
        {
            return conditionsCollection =>
            {
                if (flight.SourceIP != null && flight.SourceIP.Any())
                {
                    var addElement = conditionsCollection.CreateElement("add");
                    addElement["input"] = GetServerVariableForPivot(FlightPivots.SourceIP);
                    addElement["pattern"] = GetPatternForPivot(FlightPivots.SourceIP, flight.SourceIP);
                    conditionsCollection.Add(addElement);
                }

                if (flight.Tenant != null && flight.Tenant.Any())
                {
                    var addElement = conditionsCollection.CreateElement("add");
                    addElement["input"] = GetServerVariableForPivot(FlightPivots.Tenant);
                    addElement["pattern"] = GetPatternForPivot(FlightPivots.Tenant, flight.Tenant);
                    conditionsCollection.Add(addElement);
                }

                if (flight.User != null && flight.User.Any())
                {
                    var addElement = conditionsCollection.CreateElement("add");
                    addElement["input"] = GetServerVariableForPivot(FlightPivots.User);
                    addElement["pattern"] = GetPatternForPivot(FlightPivots.User, flight.User);
                    conditionsCollection.Add(addElement);
                }

                if (flight.DomainName != null && flight.DomainName.Any())
                {
                    var addElement = conditionsCollection.CreateElement("add");
                    addElement["input"] = GetServerVariableForPivot(FlightPivots.DomainName);
                    addElement["pattern"] = GetPatternForPivot(FlightPivots.DomainName, flight.DomainName);
                    conditionsCollection.Add(addElement);
                }
            };
        }


        private static ConfigurationElement CreateFlightRuleARR(ConfigurationElement ruleElement, Role role,
            Flight flight)
        {
            string internalUrls = string.Join(";", role.Target.Select(t => t.Redirect.GetDomainName()));
            internalUrls += ";{HTTP_HOST}";

            CreateInboundRule(ruleElement, role.Segment, role, flight.Name, flight.GetEffectiveRedirectBase(role),
                role.TargetBasePath, internalUrls, null);
            return ruleElement;
        }

        private static string GetPatternForPivot(FlightPivots pivot, List<string> pivotValue)
        {
            switch (pivot)
            {
                case FlightPivots.Default:
                    return string.Empty;

                case FlightPivots.SourceIP:
                    return GetIPRegEx(pivotValue);

                case FlightPivots.Tenant:
                    return GetExactMatchRegEx(pivotValue);

                case FlightPivots.User:
                    return GetExactMatchRegEx(pivotValue);

                case FlightPivots.DomainName:
                    return GetExactMatchRegEx(pivotValue);

                default:
                    Debug.Assert(false);
                    return String.Empty;
            }
        }

        private static ConfigurationElement CreateRoleRule(ConfigurationElement ruleElement, string roleName, Role role)
        {
            string internalUrls = role.GetEffectiveRedirectBase().GetDomainName();
            internalUrls += ";{HTTP_HOST}";

            CreateInboundRule(ruleElement, roleName, role, "Default", role.GetEffectiveRedirectBase(),
                role.TargetBasePath, internalUrls, null);
            return ruleElement;
        }

        private static void CreateInboundRule(ConfigurationElement ruleElement,
            string roleSegment,
            Role role,
            string flightName,
            string redirectBaseUri,
            string targetBasePath,
            string internalUrls, //semi-colon delimited list
            Action<ConfigurationElementCollection> addConditions)
        {
            string ruleName = String.Format("{0}{1}_{2}", RulesPrefix(), roleSegment, flightName);

            ruleElement["name"] = ruleName;
            ruleElement["patternSyntax"] = "ECMAScript";
            ruleElement["stopProcessing"] = true;

            string captureGroup = @"{R:2}";
            var matchElement = ruleElement.GetChildElement("match");
            if (!role.IsDefaultRole)
            {
                matchElement["url"] = String.Format("^{0}($|/(.*))", roleSegment);
            }
            else
            {
                // Catch-all rule
                matchElement["url"] = "(.*)";
                captureGroup = @"{R:1}";
            }

            var conditionsElement = ruleElement.GetChildElement("conditions");
            conditionsElement["trackAllCaptures"] = true;
            var conditionsCollection = conditionsElement.GetCollection();
            // Extract the protocol from the request to preserve only if the redirect uri uses it {C:1}
            bool preserveProtocol = false;
            if (!redirectBaseUri.StartsWith("http", StringComparison.OrdinalIgnoreCase))
            {
                var protocolElement = conditionsCollection.CreateElement("add");
                protocolElement["input"] = MakeVariableReference(SERVER_VARIABLE_CACHE_URL);
                protocolElement["pattern"] = "^(https?://)";
                conditionsCollection.Add(protocolElement);
                preserveProtocol = true;
            }
            // Add other conditions
            if (addConditions != null)
            {
                addConditions(conditionsCollection);
            }
            // Set some server variables for timing & rewrite (must have been previously defined in <allowedServerVariables> section)
            var variablesCollection = ruleElement.GetChildElement("serverVariables").GetCollection();
            // TODO: Add in when we have request timing
            //AddServerVariable(variablesCollection, "START_TARGETREQUEST", "{TimeProvider:}");
            string targetUri = (preserveProtocol ? @"{C:1}" : "") + redirectBaseUri + targetBasePath + "/" +
                               captureGroup;
            AddServerVariable(variablesCollection, SERVER_VARIABLE_TARGET_URI, targetUri);
            AddServerVariable(variablesCollection, SERVER_VARIABLE_ORIGINAL_ACCEPT_ENCODING,
                MakeVariableReference(SERVER_VARIABLE_ACCEPT_ENCODING));
            //Archive HTTP_ACCEPT_ENCODING before clear it out
            AddServerVariable(variablesCollection, SERVER_VARIABLE_ACCEPT_ENCODING, "");

            if (!role.IsDefaultRole)
            {
                AddServerVariable(variablesCollection, SERVER_VARIABLE_GATEWAY_ROOT_PATH,
                    string.Format("/{0}", roleSegment));
            }

            AddServerVariable(variablesCollection, SERVER_VARIABLE_INTERNAL_URLS, internalUrls);
            AddServerVariable(variablesCollection, ServerVariableOriginalHttpHost(),
                MakeVariableReference(SERVER_VARIABLE_HTTP_HOST));

            //optional Gateway server base path used to target a subfolder on the destination server
            AddServerVariable(variablesCollection, SERVER_VARIABLE_GATEWAYSERVER_ROOT_PATH, targetBasePath);

            //optionally add server variables for the outbound rule treatments
            AddTreatmentServerVariable(SERVER_VARIABLE_PROCESS_OUTBOUND_CONTENT_REWRITE, TreatmentType.ContentRewrite,
                role, variablesCollection);
            AddTreatmentServerVariable(SERVER_VARIABLE_PROCESS_OUTBOUND_CONTENT_REWRITE_RETURNURL,
                TreatmentType.ContentRewriteReturnUrl, role, variablesCollection);
            AddTreatmentServerVariable(SERVER_VARIABLE_PROCESS_OUTBOUND_LOCATION_REDIRECT,
                TreatmentType.LocationRedirect, role, variablesCollection);
            AddTreatmentServerVariable(SERVER_VARIABLE_PROCESS_OUTBOUND_CONTENT_BADCHARS, TreatmentType.BadCharsFix,
                role, variablesCollection);

            var actionElement = ruleElement.GetChildElement("action");
            actionElement["type"] = UrlRewriteActionValue(role);

            if (role.Action.HasFlag(ActionType.Redirect))
            {
                actionElement["redirectType"] = UrlRewriteRedirectType(role);
            }
            actionElement["url"] = MakeVariableReference(SERVER_VARIABLE_TARGET_URI);
        }

        private static ConfigurationElement CreateInboundGwRemoteUserRule(ConfigurationElement ruleElement)
        {
            string ruleName = string.Format("{0}{1}", RulesPrefix(), "Set User + Tenant");

            ruleElement["name"] = ruleName;
            ruleElement["patternSyntax"] = "ECMAScript";
            ruleElement["stopProcessing"] = false;

            var matchElement = ruleElement.GetChildElement("match");
            matchElement["url"] = "(.*)";

            var conditionsElement = ruleElement.GetChildElement("conditions");
            conditionsElement["trackAllCaptures"] = true;
            var conditionsCollection = conditionsElement.GetCollection();

            //Get the username from the UPN part of the HTTP_COOKIE
            var protocolElement = conditionsCollection.CreateElement("add");
            protocolElement["input"] = MakeVariableReference(SERVER_VARIABLE_HTTP_COOKIE);
            protocolElement["pattern"] = @"(;| )?UPN=([^@]+@([^;]+))";
            conditionsCollection.Add(protocolElement);

            // set the server variable
            var variablesCollection = ruleElement.GetChildElement("serverVariables").GetCollection();
            AddServerVariable(variablesCollection, SERVER_VARIABLE_HTTP_X_REMOTE_USER, "{C:2}");
            AddServerVariable(variablesCollection, SERVER_VARIABLE_HTTP_X_TENANT, "{C:3}");

            var actionElement = ruleElement.GetChildElement("action");
            actionElement["type"] = "None";

            return ruleElement;
        }

        private static void CreateOutboundRewriteRule(ConfigurationElementCollection outboundRules,
            ConfigurationElementCollection preConditions)
        {
            string ruleName = string.Format("{0}{1}", RulesPrefix(), "Shared Content Rewrite");
            //create the precondition
            preConditions.Add(CreatePrecondition(preConditions.CreateElement("preCondition"), ruleName,
                Tuple.Create(SERVER_VARIABLE_PROCESS_OUTBOUND_CONTENT_REWRITE, "^1$"),
                Tuple.Create(SERVER_VARIABLE_RESPONSE_CONTENT_TYPE, "^text/html")));

            var ruleElement = outboundRules.CreateElement("rule");

            ruleElement["name"] = ruleName;
            ruleElement["preCondition"] = ruleName;

            //Match URL
            //**See the detailed comments in the SetOutboundRuleConditions method for a greater understanding of the rules**
            ConfigurationElement matchElement = ruleElement.GetChildElement("match");
            matchElement["filterByTags"] = @"A, Area, Base, Form, Frame, Head, IFrame, Img, Input, Link, Script";
            matchElement["pattern"] = @"(.*)"; 

            //Add conditons (shared between rules)
            SetOutboundRuleConditions(ruleElement, OutboundRuleType.SharedContentRewrite);

            //- "{C:6}{GATEWAY_ROOT_PATH}{C:9}"
            //- {C:6} is the back reference to the protocol and domain name. It will be empty for a path that is relative to the root of the site.
            //- {GATEWAY_ROOT_PATH} is the path segment (aka role segment) on the gateway server
            //- {C:9} is the rest of the path which has the gateway and webserver's base paths removed.
            ConfigurationElement actionElement = ruleElement.GetChildElement("action");
            actionElement["type"] = @"Rewrite";
            actionElement["value"] = string.Format("{{C:6}}{0}/{{C:9}}",
                MakeVariableReference(SERVER_VARIABLE_GATEWAY_ROOT_PATH));

            outboundRules.Add(ruleElement);
        }

        private static void CreateOutboundReturnUrlRewriteRule(ConfigurationElementCollection outboundRules,
            ConfigurationElementCollection preConditions)
        {
            string ruleName = string.Format("{0}{1}", RulesPrefix(), "Shared Content Rewrite - ReturnUrl");
            //create the precondition
            preConditions.Add(CreatePrecondition(preConditions.CreateElement("preCondition"), ruleName,
                Tuple.Create(SERVER_VARIABLE_PROCESS_OUTBOUND_CONTENT_REWRITE_RETURNURL, "^1$"),
                Tuple.Create(SERVER_VARIABLE_RESPONSE_CONTENT_TYPE, "^text/html")));

            var ruleElement = outboundRules.CreateElement("rule");

            ruleElement["name"] = ruleName;
            ruleElement["preCondition"] = ruleName;

            //Match URL
            //For the ReturnUrl treatment, there's no need to worry about the rest of the string after the return URL 
            //(for example &articleId=5 in http://domain.com?id=5&ReturnUrl=www.domain2.com&articleId=5)  because it 
            //will be left alone and pieced back together as it was.
            ConfigurationElement matchElement = ruleElement.GetChildElement("match");
            matchElement["filterByTags"] = @"A, Area, Base, Form, Frame, Head, IFrame, Img, Input, Link, Script";
            matchElement["pattern"] = String.Format("(.*?|&)({0})=([^&]+)(.*)", RETURNURL_PARAMETER_NAMES);

            //Add conditons (shared between rules)
            SetOutboundRuleConditions(ruleElement, OutboundRuleType.SharedContentRewriteReturnUrl);

            //{R:1} is the original string before the return url section
            //{R:2} is the return URL parameter (e.g. ReturnUrl, wreply, etc)
            //Either {C:11} or {C:12} will be set depending on whether the original return url path was url encoded or not
            //{R:4} is for possible querystring parts after the return url
            ConfigurationElement actionElement = ruleElement.GetChildElement("action");
            actionElement["type"] = @"Rewrite";
            actionElement["value"] = "{R:1}{R:2}={C:11}{C:12}{R:4}";

            outboundRules.Add(ruleElement);
        }

        private static void CreateOutboundLocationRedirectRule(ConfigurationElementCollection outboundRules,
            ConfigurationElementCollection preConditions)
        {
            string ruleName = string.Format("{0}{1}", RulesPrefix(), "Shared Location Redirect");
            //create the precondition
            preConditions.Add(CreatePrecondition(preConditions.CreateElement("preCondition"), ruleName,
                Tuple.Create(SERVER_VARIABLE_PROCESS_OUTBOUND_LOCATION_REDIRECT, "^1$"),
                Tuple.Create(SERVER_VARIABLE_RESPONSE_STATUS, @"^3\d\d$")));

            var ruleElement = outboundRules.CreateElement("rule");

            ruleElement["name"] = ruleName;
            ruleElement["preCondition"] = ruleName;

            //Match URL
            //**See the detailed comments in the SetOutboundRuleConditions method for a greater understanding of the rules**
            ConfigurationElement matchElement = ruleElement.GetChildElement("match");
            matchElement["serverVariable"] = SERVER_VARIABLE_RESPONSE_LOCATION;
            matchElement["pattern"] = @"(.*)"; 

            //Conditions setup
            ConfigurationElement conditionsElement = ruleElement.GetChildElement("conditions");
            ConfigurationElementCollection conditionsCollection = conditionsElement.GetCollection();
            conditionsElement["logicalGrouping"] = @"MatchAll";
            conditionsElement["trackAllCaptures"] = true;

            //Add conditons (shared between rules)
            SetOutboundRuleConditions(ruleElement, OutboundRuleType.SharedLocationRedirect);

            //- "{C:6}{GATEWAY_ROOT_PATH}{C:9}"
            //- {C:6} is the back reference to the protocol and domain name. It will be empty for a path that is relative to the root of the site.
            //- {GATEWAY_ROOT_PATH} is the path segment (aka role segment) on the gateway server
            //- {C:9} is the rest of the path which has the gateway and webserver's base paths removed.
            ConfigurationElement actionElement = ruleElement.GetChildElement("action");
            actionElement["type"] = @"Rewrite";
            actionElement["value"] = string.Format("{{C:6}}{0}/{{C:9}}",
                MakeVariableReference(SERVER_VARIABLE_GATEWAY_ROOT_PATH));

            outboundRules.Add(ruleElement);
        }

        private static void CreateOutboundRemoveBadcharsRuleBegin(ConfigurationElementCollection outboundRules)
        {
            var ruleElement = outboundRules.CreateElement("rule");
            string ruleName = string.Format("{0}{1}", RulesPrefix(), "Remove <![");

            ruleElement["name"] = ruleName;
            ruleElement["preCondition"] = string.Format("{0}{1}", RulesPrefix(), ELEMENT_OUTBOUND_REMOVE_BADCHARS_PRECONDITION);

            ConfigurationElement matchElement = ruleElement.GetChildElement("match");
            matchElement["filterByTags"] = @"";
            //Handle the [ has an escaped regular expression character
            matchElement["pattern"] = ELEMENT_OUTBOUND_REMOVE_BADCHARS_RULE_PROBLEM_STRING_BEING;

            ConfigurationElement actionElement = ruleElement.GetChildElement("action");
            actionElement["type"] = @"Rewrite";
            //replace the problem string pattern with the temporary string
            actionElement["value"] = ELEMENT_OUTBOUND_REMOVE_BADCHARS_RULE_TEMP_STRING;

            outboundRules.Add(ruleElement);
        }

        private static void CreateOutboundRemoveBadcharsRuleEnd(ConfigurationElementCollection outboundRules)
        {
            var ruleElement = outboundRules.CreateElement("rule");
            string ruleName = string.Format("{0}{1}", RulesPrefix(), "Return <![");

            ruleElement["name"] = ruleName;
            ruleElement["preCondition"] = string.Format("{0}{1}", RulesPrefix(), ELEMENT_OUTBOUND_REMOVE_BADCHARS_PRECONDITION);

            ConfigurationElement matchElement = ruleElement.GetChildElement("match");
            matchElement["filterByTags"] = @"";
            matchElement["pattern"] = ELEMENT_OUTBOUND_REMOVE_BADCHARS_RULE_TEMP_STRING;

            ConfigurationElement actionElement = ruleElement.GetChildElement("action");
            actionElement["type"] = @"Rewrite";
            //put the problem string pattern back again
            actionElement["value"] = ELEMENT_OUTBOUND_REMOVE_BADCHARS_RULE_PROBLEM_STRING_END;

            outboundRules.Add(ruleElement);
        }

        private static void CreateOutboundRestoreAcceptEncodingRule(ConfigurationElementCollection outboundRules,
            ConfigurationElementCollection preConditions)
        {
            string ruleName = string.Format("{0}{1}", RulesPrefix(), "RestoreAcceptEncoding");

            preConditions.Add(CreatePrecondition(preConditions.CreateElement("preCondition"), ruleName,
                Tuple.Create(SERVER_VARIABLE_ORIGINAL_ACCEPT_ENCODING, @".+")));

            outboundRules.Add(CreateRewriteOutboundRule(
                outboundRules.CreateElement("rule"),
                ruleName,
                ruleName,
                "serverVariable",
                SERVER_VARIABLE_ACCEPT_ENCODING,
                @"(.*)",
                @"Rewrite",
                MakeVariableReference(SERVER_VARIABLE_ORIGINAL_ACCEPT_ENCODING)
                ));
        }

        private static void SetOutboundRuleConditions(ConfigurationElement ruleElement, OutboundRuleType ruleType)
        {
            //Treatments regex documentation is included below throughout this method

            //High level Pseudo-code
            //- If domain name is empty 
            //   - If Path (e.g. /role/page.aspx?id=5) starts with a / then continue
            //   - If it doesn't start with / then stop running this treatment for this URL
            //- If domain name is not empty then 
            //   - If it matches the internal urls then continue
            //   - If it doesn't match the internal URL then stop running this treatment for this URL
            //- If Path starts with the gateway or webserver's root path then remove them and replace them with the gateway's root path.
            //- The action should maintain the protocol and put in the original http host if the original was a FQDN, otherwise it should retain the relative URL.

            //Conditions setup
            ConfigurationElement conditionsElement = ruleElement.GetChildElement("conditions");
            ConfigurationElementCollection conditionsCollection = conditionsElement.GetCollection();
            conditionsElement["logicalGrouping"] = @"MatchAll";
            conditionsElement["trackAllCaptures"] = true;

            // First condition - Tease apart the URL pattern
            //
            // the URL pattern to treat: This comes from {R:1} for two of the outbound rules and {R:3} for the return URL rule.
            // Example {R:1}: https://www.domain.com/role/page.aspx?id=5
            //
            // The following will be set in the back references 
            // C:0 https://www.domain.com/role/page.aspx?id=5
            // C:1 https:// or http:// or //   (Call it "Protocol" for notes below)
            // C:2 www.domain.com              (Call it "Domain name")
            // C:3 /role/page.aspx?id=5        (Call it "Path")
            //
            // (?:(https://|http://|//)([^/:?#]+)(?::\d*)?)? gets the optional protocol and domain name and port. 
            // Note: The port isn't included in a back reference since the rewritten URL is assumed to be over port 80 or 443. 
            //       Update this in the future to support different ports in the rewritten URL if that assumption needs to be revised.
            // "([^/:?#]+)" checks for all characters until the first : ? or #.
            // ?: simply means that the capture group isn't added to the back references

            //This first condition is slightly different between the outbound rules, so branch for this condition only
            string condition1Input;
            string condition1Pattern;
            switch (ruleType)
            {
                case OutboundRuleType.SharedContentRewrite:
                case OutboundRuleType.SharedLocationRedirect:
                    condition1Input = "{R:1}";
                    condition1Pattern = @"^(?:(https://|http://|//)([^/:?#]+)(?::\d*)?)?(.*)";
                    break;
                case OutboundRuleType.SharedContentRewriteReturnUrl:
                    condition1Input = "{UrlDecode:{R:3}}";
                    condition1Pattern = @"^(?:(https://|http://|//)([^/:?#]+)(?::\d*)?)?(.*)";
                    break;
                default:
                    throw new Exception(String.Format("Rule type {0} not set in SetOutboundRuleConditions", ruleType.ToString()));
            }

            ConfigurationElement addElement1 = conditionsCollection.CreateElement("add");
            addElement1["input"] = condition1Input;
            addElement1["pattern"] = condition1Pattern;
            conditionsCollection.Add(addElement1);

            // Second condition - Get the domain name *IF* the URL pattern is for a FQDN and it is in the list of Internal URLs
            // This condition always passes and is used for the back reference only.
            // If the domain name matches one of the Internal URLs then {C:5} will have the domain name
            // If the domain name is empty or doesn't match the Internal URLs then {C:5} will be empty
            //
            // Internal URLs is a semi-colon delimited list of URLs to rewrite. It includes the web server(s) URL(s) plus the original HTTP_HOST (domain name).
            // {C:4} is not important outside of this condition
            //
            // Example: Internal URLs = internalsite.com, gatewaydomain.com
            //          {C:2} = gatewaydomain.com
            //          In this case {C:5} will be internalsite.com
            // Example2: Internal URLs = internalsite.com, gatewaydomain.com
            //          {C:2} = www.microsoft.com
            //          In this case {C:5} will be empty (since we don't want to change www.microsoft.com in any URL patterns)
            // Example3: Internal URLs = internalsite.com, gatewaydomain.com
            //          {C:2} = empty (it was a relative URL)
            //          In this case {C:5} will be empty 
            //
            // \1 is a back reference to the domain name 
            // "(?:.*;)?(\1)(?:;|$)" checks for the domain name (\1) in the list of Internal URLs. 
            //       Checks that it's at the beginning of the string or right after a ;, and that it's at the end of the string or before a ;
            // If there isn't a match, the "|.*" ensures that it still passes with an empty back reference
            ConfigurationElement addElement2 = conditionsCollection.CreateElement("add");
            addElement2["input"] = string.Format(@"{{C:2}}~{0}",
                MakeVariableReference(SERVER_VARIABLE_INTERNAL_URLS));
            addElement2["pattern"] = @"^(.+)~(?:.*;)?(\1)(?:;|$)|.*";
            conditionsCollection.Add(addElement2);

            // This condition is used along with the previous condition to stop processing
            //   the URL if the FQDN isn't from the Internal URLs (e.g. don't rewrite for www.not_related.com)
            // {C:2} is the domain name. {C:5} is either a domain name or empty string from the previous condition.
            // "^[^~]+~$" is a match if {C:2} is not empty but {C:5} value is empty. The condition passes if it *isn't* true.
            //
            // To put it another way, this ensures that either it's a relative path or the FQDN is relevant
            //
            // Example: {C:2} is www.internalsite.com. {C:5} is www.internalsite.com.
            //              This fails the regex, but it's negated, so this allows the rule to continue.
            // Example2: {C:2} is empty. {C:5} is also empty.
            //              This fails the regex, but it's negated, so this allows the rule to continue.
            // Example3: {C:2} is www.not_related.com. {C:5} is empty because the FQDN isn't applicable.
            //              This passes the regex, and since it's negated, this causes the rule to stop processing this URL
            ConfigurationElement addElement3 = conditionsCollection.CreateElement("add");
            addElement3["input"] = @"{C:2}~{C:5}";
            addElement3["pattern"] = @"^[^~]+~$";
            addElement3["negate"] = true;
            conditionsCollection.Add(addElement3);

            // This condition simply sets the original protocol plus the original domain name (e.g. www.gatewaydomain.com) 
            //    to be used as a backreference 
            // For a FQDN, {C:6} will return the protocol + domain name
            // For a relative path, {C:6} will be empty
            // This condition always passes and is used for the backreference only
            ConfigurationElement addElement4 = conditionsCollection.CreateElement("add");
            addElement4["input"] = string.Format(@"{{C:1}}{0}",
                MakeVariableReference(ServerVariableOriginalHttpHost()));
            addElement4["pattern"] = @"^(.*//.*)|.*";
            conditionsCollection.Add(addElement4);

            // This condition removes the {GATEWAY_ROOT_PATH} and {WEBSERVER_ROOT_PATH} from the path so that it can be put back again as a cleaned up URL
            // This always passes and is used for the backreference only
            // The following regex is used 3 times: (?:\1(?=[/]|$))  (first and third are for GATEWAY_ROOT_PATH and the second is for WEBSERVER_ROOT_PATH) ...
            //       so that the two variables (gateway or web) can be in either order.
            // {C:9} will return the path without either {GATEWAY_ROOT_PATH} or {WEBSERVER_ROOT_PATH}
            // ?= looks forward to the / but doesn't include it in the capture group. Example: A string of abc/def and a regex of .*(?=[/]) will return a 
            //       string of abc, and the / can still be used for the rest of the regex matching.
            ConfigurationElement addElement5 = conditionsCollection.CreateElement("add");
            addElement5["input"] = string.Format(@"{0}~{1}~{{C:3}}",
                MakeVariableReference(SERVER_VARIABLE_GATEWAY_ROOT_PATH),
                MakeVariableReference(SERVER_VARIABLE_GATEWAYSERVER_ROOT_PATH));
            addElement5["pattern"] = @"^([^~]*)~([^~]*)~(?:\1(?=[/]|$))?(?:\2(?=[/]|$))?(?:\1(?=[/]|$))?/?(.*)";
            conditionsCollection.Add(addElement5);

            //This condition is for the ReturnUrl pattern only and it determines whether to re-encode the URL or not
            //The original pattern is made up of four parts: {the original return URL}~{the decoded return URL}~{the new updated complete URL path}~{the new encoded updated complete URL path}
            //If the first 2 sections match then we assume that the pattern wasn't encoded to start, and we return the 3rd section
            //If the first 2 sections don't match then we assume that the pattern was encoded to start, and we return the 4th section
            //The regex functions like an if/else.
            //For: ^([^~]+)~(?:\1~(.*)~|.*~.*~(.*)).
            //    ([^~]+)~ gets the first section
            //    (?: ) is a non-capturing section
            //    \1~(.*)~ using the backreference of \1 to the first section, this sees if the 1st and 2nd section match and returns the 3rd section
            //    |.*~.*~(.*) if the previous didn't match then it will instead return the 4th section
            //
            //If the original wasn't URL Encoded then {C:11} will have the complete return URL path. {C:12} will be empty.
            //If the original was URL Encoded then {C:12} will have the complete return URL path. {C:11} will be empty.
            //
            //Note: some URL patterns are only partially encoded (e.g. http:%2f%2fsomething. Note the : isn't encoded), so the final re-encoded URL may encode it
            //   more than the original. We make an assumption that this is ok, but watch for this in real-world situations to see if it isn't.
            if (ruleType == OutboundRuleType.SharedContentRewriteReturnUrl) 
            { 
                ConfigurationElement addElement6 = conditionsCollection.CreateElement("add");
                addElement6["input"] = String.Format(@"{{R:3}}~{{UrlDecode:{{R:3}}}}~{{C:6}}{0}/{{C:9}}~{{UrlEncode:{{C:6}}{0}/{{C:9}}}}", 
                        MakeVariableReference(SERVER_VARIABLE_GATEWAY_ROOT_PATH));
                addElement6["pattern"] = @"^([^~]+)~(?:\1~(.*)~|.*~.*~(.*))";
                conditionsCollection.Add(addElement6);
            }


            // Ignore relative paths
            // Example: "vdir/file.aspx" or "javascript:something" or "mailto:something"
            // Since the rule is negated, if the URL is a relative path then stop processing this URL
            ConfigurationElement addElement7 = conditionsCollection.CreateElement("add");
            addElement7["input"] = @"{C:2}~{C:3}";
            addElement7["pattern"] = @"^~(?:[^/])";
            addElement7["negate"] = true;
            conditionsCollection.Add(addElement7);
        }

        private static ConfigurationElement CreateRewriteOutboundRule(ConfigurationElement rule,
            string ruleName,
            string preConditionName,
            string matchSubject,
            string matchValue,
            string matchPattern,
            string actionType,
            string actionValue)
        {
            rule["name"] = ruleName;
            rule["preCondition"] = preConditionName;

            var matchElement = rule.GetChildElement("match");
            matchElement[matchSubject] = matchValue;
            matchElement["pattern"] = matchPattern;

            var action = rule.GetChildElement("action");
            action["type"] = actionType;
            action["value"] = actionValue;

            return rule;
        }

        private static ConfigurationElement CreatePrecondition(ConfigurationElement preCondition, string conditionName,
            params Tuple<string, string>[] matchArguments)
        {
            preCondition["name"] = conditionName;
            var preConditionCollection = preCondition.GetCollection();
            foreach (var conditionMatch in matchArguments)
            {
                var matchElement = preConditionCollection.CreateElement("add");
                matchElement["input"] = MakeVariableReference(conditionMatch.Item1);
                matchElement["pattern"] = conditionMatch.Item2;
                preConditionCollection.Add(matchElement);
            }
            return preCondition;
        }

        private static void ClearExistingRules(ConfigurationElementCollection preConditionsCollection,
            ConfigurationElementCollection inboundRulesCollection, 
            ConfigurationElementCollection outboundRulesCollection)
        {
            var rulesPrefix = RulesPrefix();

            foreach (var inboundRule in inboundRulesCollection.ToList().Where(inboundRule => inboundRule["name"].ToString().StartsWith(rulesPrefix)))
            {
                inboundRulesCollection.Remove(inboundRule);
            }
            foreach (var outboundRule in outboundRulesCollection.ToList().Where(outboundRule => outboundRule["name"].ToString().StartsWith(rulesPrefix)))
            {
                outboundRulesCollection.Remove(outboundRule);
            }
            foreach (var precondition in preConditionsCollection.ToList().Where(precondition => precondition["name"].ToString().StartsWith(rulesPrefix)))
            {
                preConditionsCollection.Remove(precondition);
            }
        }

        private static string GetServerVariableForPivot(FlightPivots pivot)
        {
            switch (pivot)
            {
                case FlightPivots.Default:
                    return String.Empty;

                case FlightPivots.SourceIP:
                    return MakeVariableReference(SERVER_VARIABLE_REMOTE_ADDR);

                case FlightPivots.Tenant:
                    return MakeVariableReference(SERVER_VARIABLE_HTTP_X_TENANT);

                case FlightPivots.User:
                    return MakeVariableReference(SERVER_VARIABLE_HTTP_X_REMOTE_USER);

                case FlightPivots.DomainName:
                    return MakeVariableReference(SERVER_VARIABLE_HTTP_HOST);

                default:
                    Debug.Assert(false);
                    return String.Empty;
            }
        }

        private static string MakeVariableReference(string variableName)
        {
            return '{' + variableName + '}';
        }

        private static void AddServerVariable(ConfigurationElementCollection variablesCollection, string name,
            string value)
        {
            var variableElement = variablesCollection.CreateElement("set");
            variableElement["name"] = name;
            variableElement["value"] = value;
            variablesCollection.Add(variableElement);
        }

        private static void AddTreatmentServerVariable(string serverVariable, TreatmentType treatmentType, Role role,
            ConfigurationElementCollection variablesCollection)
        {
            if (role.Treatment.HasFlag(treatmentType))
            {
                AddServerVariable(variablesCollection, serverVariable, "1");
            }
        }


        /// <summary>
        /// Generate a regex to validate the IP given in the Flight Value property                
        /// </summary>
        /// <param name="value">Flight SourceIP pivot value</param>
        /// <returns>string representing a regex for the IP</returns>
        private static string GetIPRegEx(List<string> IPs)
        {
            if (!IPs.Any())
            {
                throw new ArgumentNullException(
                    "Flight Value must be non null, e.g 1.2.3.4 for exact match and 1.2.3. for match on ip range 1.2.3.0 - 1.2.3.255");
            }

            //otherwise generate exact match expression or wildcard
            // e.g input  1.2.3.4 retun a regex with exact match ^1\.2\.3\.4$
            //     input with a missing part requires "wildcard" expression similar to provide 0-255 for that missing part
            //      example input of 1.2.3 will generate regex ^1\.2\.3\.([0]|[1-9]\\d?|[1]\\d{2}|2([0-4]\\d|5[0-5]))$          

            var matchRules = new List<string>();
            foreach (var ip in IPs)
            {
                var parts = ip.Split('.');
                if (parts.Length < 4)
                {
                    Array.Resize(ref parts, 4); //pad the array
                }

                var expression = string.Join(SLASH_DOT,
                    parts.Select(part => string.IsNullOrWhiteSpace(part) ? REGEX_0_TO_255 : part));
                matchRules.Add(string.Format("(^{0}$)", expression));
            }

            return string.Join("|", matchRules);
        }

        /// <summary>
        /// Generate an exact match regex 
        /// </summary>
        /// <param name="value">Flight Tenant, user pivot value, or DomainName value</param>
        /// <returns>RegEx representing the Flight tenant or user pivot value</returns>
        private static string GetExactMatchRegEx(List<string> matchCriteria)
        {
            if (!matchCriteria.Any())
            {
                throw new ArgumentNullException("Flight Value must be non null");
            }

            // generate a regex that is used to validate flights using either user or tenant against the 
            // HTTP_X_TENANT and HTTP_X_REMOTE_USER server variables
            // e.g HTTP_X_REMOTE_USER = nickha@microsoft.com resulting regex = (^nickha@microsoft\.com$)
            // if the provided value is a comma separated string e.g nickha@microsoft.com, nickha@bing.com generate (^nickha@microsoft\.com$)|(^nickha@bing\.com$)
            // e.g HTTP_X_TENANT = microsoft.com resulting regex = (^microsoft\.com$)
            // if the provided value is a comma separated string e.g microsoft.com, bing.com generate (^microsoft\.com$)|(^bing\.com$)

            return string.Join("|", matchCriteria
                .Select(match => string.Format("(^{0}$)", match.Replace(".", SLASH_DOT).Trim())));
        }

        /// <summary>
        /// Get the prefix for rules and preconditions. This is required so that clearing the gateway rules don't clear non-gateway rules
        /// </summary>
        /// <returns>The prefix as a string</returns>
        private static string RulesPrefix()
        {
            return AzureUtils.GetConfigSetting(ConfigKeyRulesPrefix, DEFAULT_CONFIG_KEY_RULES_PREFIX);
        }

        /// <summary>
        /// Get the ORIGINAL_HTTP_HOST server variable name, allowing it to be overridden
        /// </summary>
        /// <returns>The server variable name</returns>
        private static string ServerVariableOriginalHttpHost()
        {
            return AzureUtils.GetConfigSetting(ConfigKeyServerVariableOriginalHttpHost, SERVER_VARIABLE_ORIGINAL_HTTP_HOST);
        }

        private static string UrlRewriteActionValue(Role role)
        {
            switch (role.Action)
            {
                case ActionType.Rewrite:
                    return "Rewrite";

                case ActionType.FoundRedirect:
                case ActionType.PermanentRedirect:
                case ActionType.SeeOtherRedirect:
                case ActionType.TemporaryRedirect:
                    return "Redirect";

                default:
                    Trace.TraceWarning("Unsupported rewrite action [{0}] for role [{1}]. Defaulting to 'Rewrite'", role.Action, role.Segment);
                    return "Rewrite";
            }
        }

        private static string UrlRewriteRedirectType(Role role)
        {
            switch (role.Action)
            {
                case ActionType.FoundRedirect:
                    return "Found";

                case ActionType.PermanentRedirect:
                    return "Permanent";

                case ActionType.SeeOtherRedirect:
                    return "SeeOther";

                case ActionType.TemporaryRedirect:
                    return "Temporary";

                default:
                    Trace.TraceWarning("Unsupported redirect action [{0}] for role [{1}]. Defaulting to 'Temporary'");
                    return "Temporary";
            }
        }
    }

    public static class StringExtensions
    {
        public static string GetDomainName(this String str)
        {
            if (str.IndexOf(@"://", System.StringComparison.Ordinal) < 0)
            {
                str = string.Format("http://{0}", str);
            }
            Uri uri = new Uri(str);
            return uri.Host;
        }
    }

    enum OutboundRuleType
    {
        SharedContentRewrite,
        SharedContentRewriteReturnUrl,
        SharedLocationRedirect
    }
}
